Creating Structured Forms in Flutter
Structured forms are an important part of Flutter application development. A structured form organizes multiple input fields into a single logical unit so that user data can be collected, validated, saved, reset, and submitted efficiently. Flutter provides the Form, FormField, and TextFormField widgets for building structured forms. The Form widget acts as a common container for multiple form fields and provides access to operations such as validation, saving, and resetting. :contentReference[oaicite:0]{index=0}
1. What is a Structured Form?
A structured form is a collection of related input controls arranged in a meaningful order. Instead of treating every input field independently, a form groups related fields together and manages their behavior as one unit.
Example
A student registration form can contain:
- Student Name
- Email Address
- Phone Number
- Date of Birth
- Course
- Password
- Confirm Password
- Terms and Conditions
- Submit Button
- Reset Button
All these fields can be placed inside one Flutter Form.
2. Why Use Structured Forms?
Structured forms make applications easier to develop, maintain, validate, and use.
- Group related fields together.
- Validate multiple fields at once.
- Display meaningful validation messages.
- Save field values systematically.
- Reset fields when required.
- Manage keyboard focus.
- Control user input.
- Submit data to APIs or databases.
- Improve the user experience.
- Make large forms easier to maintain.
3. Main Flutter Form Widgets
| Widget/Class | Purpose |
|---|
Form | Groups multiple form fields and manages form-level state. |
FormField | Represents an individual form field with its own state and validation. |
TextFormField | Provides text input integrated with the Form system. |
TextEditingController | Reads and controls text entered by the user. |
GlobalKey | Provides access to the FormState for validation, saving, and resetting. |
FocusNode | Controls keyboard focus between form fields. |
InputDecoration | Controls labels, hints, borders, icons, and other field decoration. |
Flutter's API describes Form as an optional container for grouping multiple form fields and using FormState to save, reset, or validate those fields. :contentReference[oaicite:1]{index=1}
4. Basic Structure of a Structured Form
Form(
key: formKey,
child: Column(
children: [
TextFormField(),
TextFormField(),
ElevatedButton(
onPressed: submitForm,
child: const Text('Submit'),
),
],
),
)
A typical structured form contains:
- A form key.
- Input fields.
- Validation rules.
- Data collection logic.
- Submit functionality.
- Reset functionality.
- Optional focus management.
5. Creating GlobalKey
A GlobalKey is commonly used when the application needs to access the state of a form. It allows code to call methods such as validate(), save(), and reset(). Flutter's documentation recommends using a GlobalKey as a straightforward way to access form state. :contentReference[oaicite:2]{index=2}
final GlobalKey formKey =
GlobalKey();
The key is then assigned to the Form:
Form(
key: formKey,
child: Column(
children: [
TextFormField(),
],
),
)
6. Why Should the Form Key Be Stored?
The form key should normally be created once and stored as part of the state rather than being recreated every time build() runs.
class RegistrationScreen extends StatefulWidget {
const RegistrationScreen({super.key});
@override
State createState() =>
_RegistrationScreenState();
}
class _RegistrationScreenState
extends State {
final formKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: const Column(
children: [],
),
);
}
}
7. TextFormField in Structured Forms
TextFormField is especially useful in structured forms because it integrates a text field with Flutter's FormField system and supports validation and saving. :contentReference[oaicite:3]{index=3}
TextFormField(
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
)
8. Creating a Basic Structured Form
import 'package:flutter/material.dart';
class UserForm extends StatefulWidget {
const UserForm({super.key});
@override
State createState() => _UserFormState();
}
class _UserFormState extends State {
final formKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User Form'),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Form(
key: formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {},
child: const Text('Submit'),
),
],
),
),
),
);
}
}
9. Organizing Fields into Sections
Large forms should be divided into logical sections instead of displaying a long list of unrelated fields.
Example Sections
- Personal Information
- Contact Information
- Course Information
- Account Information
- Terms and Conditions
Example
Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Personal Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(),
const SizedBox(height: 16),
TextFormField(),
const SizedBox(height: 30),
const Text(
'Contact Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(),
],
),
)
10. Using Form Validation
Validation ensures that users provide acceptable information before the application processes the form. Flutter's validator callback returns an error message when a field is invalid and returns null when the value is valid. :contentReference[oaicite:4]{index=4}
Required Field
TextFormField(
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Name is required';
}
return null;
},
)
11. Validating the Entire Form
When the user presses Submit, call validate() on the form state.
void submitForm() {
if (formKey.currentState!.validate()) {
print('Form is valid');
} else {
print('Form contains errors');
}
}
When validate() is called, Flutter runs the validators associated with the form fields. It returns true when there are no validation errors and false when validation errors exist. :contentReference[oaicite:5]{index=5}
12. Required Field Validation
TextFormField(
decoration: const InputDecoration(
labelText: 'Full Name',
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter your full name';
}
return null;
},
)
13. Email Field
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email Address',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Enter a valid email address';
}
return null;
},
)
14. Phone Number Field
TextFormField(
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Phone number is required';
}
if (value.trim().length < 10) {
return 'Enter a valid phone number';
}
return null;
},
)
15. Password Field
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must contain at least 8 characters';
}
return null;
},
)
16. Confirm Password Field
A structured registration form often contains a password confirmation field. The second password can be compared with the first password.
final passwordController = TextEditingController();
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
)
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Confirm Password',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please confirm your password';
}
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
},
)
17. Using TextEditingController
A TextEditingController is useful when the application needs direct access to field values, needs to prefill fields, or needs to modify and clear input programmatically. Flutter's documentation also recommends disposing the controller when it is no longer needed. :contentReference[oaicite:6]{index=6}
final nameController = TextEditingController();
TextFormField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Name',
),
)
Reading the Value
final name = nameController.text.trim();
Clearing the Value
nameController.clear();
18. Managing Multiple Controllers
A larger form may require multiple controllers.
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final addressController = TextEditingController();
final passwordController = TextEditingController();
Dispose Controllers
@override
void dispose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
addressController.dispose();
passwordController.dispose();
super.dispose();
}
19. Collecting Form Data
After successful validation, controller values can be collected and converted into variables or a model.
void submitForm() {
if (!formKey.currentState!.validate()) {
return;
}
final name = nameController.text.trim();
final email = emailController.text.trim();
final phone = phoneController.text.trim();
final address = addressController.text.trim();
print('Name: $name');
print('Email: $email');
print('Phone: $phone');
print('Address: $address');
}
20. Using onSaved
Instead of using controllers for every field, a FormField can use its onSaved callback to store the value when FormState.save() is called.
String name = '';
String email = '';
TextFormField(
onSaved: (value) {
name = value?.trim() ?? '';
},
)
TextFormField(
onSaved: (value) {
email = value?.trim() ?? '';
},
)
Calling save()
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
print(name);
print(email);
}
FormState.save() calls the onSaved callback of each descendant form field. :contentReference[oaicite:7]{index=7}
21. Resetting a Structured Form
Flutter provides FormState.reset() to reset the form fields and their validation state.
formKey.currentState!.reset();
Reset Button
OutlinedButton(
onPressed: () {
formKey.currentState!.reset();
},
child: const Text('Reset'),
)
If controllers are being used, clear them as well when the intended behavior is to empty the text fields.
void resetForm() {
formKey.currentState!.reset();
nameController.clear();
emailController.clear();
phoneController.clear();
}
22. Automatic Validation
autovalidateMode controls when form fields automatically display validation errors.
| Mode | Meaning |
|---|
AutovalidateMode.disabled | Automatic validation is disabled. |
AutovalidateMode.always | Validation runs automatically. |
AutovalidateMode.onUserInteraction | Validation occurs as the user interacts with the field. |
Example
TextFormField(
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.isEmpty) {
return 'This field is required';
}
return null;
},
)
23. Handling Form Changes
The Form widget provides an onChanged callback that is called when a form field changes. :contentReference[oaicite:8]{index=8}
Form(
key: formKey,
onChanged: () {
print('Form changed');
},
child: Column(
children: [
TextFormField(),
TextFormField(),
],
),
)
24. Managing Keyboard Focus
Good focus management makes forms easier to use. Users can move from one field to another using the keyboard's Next action. Flutter provides FocusNode and FocusScope for managing focus. :contentReference[oaicite:9]{index=9}
Create FocusNodes
final nameFocus = FocusNode();
final emailFocus = FocusNode();
final phoneFocus = FocusNode();
Connect FocusNode
TextFormField(
focusNode: nameFocus,
textInputAction: TextInputAction.next,
)
Move to Next Field
TextFormField(
focusNode: nameFocus,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(emailFocus);
},
)
25. Dispose FocusNodes
Focus nodes are long-lived objects, so their lifecycle should be managed by the stateful widget that owns them. :contentReference[oaicite:10]{index=10}
@override
void dispose() {
nameFocus.dispose();
emailFocus.dispose();
phoneFocus.dispose();
super.dispose();
}
26. Input Formatting
Input formatters can be used to restrict the type or length of information entered into a field.
Numbers Only
import 'package:flutter/services.dart';
TextFormField(
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
labelText: 'PIN Code',
),
)
Limit Characters
TextFormField(
maxLength: 50,
decoration: const InputDecoration(
labelText: 'Username',
),
)
27. Dropdown in a Structured Form
Structured forms can contain selection fields as well as text fields.
String? selectedCourse;
DropdownButtonFormField(
decoration: const InputDecoration(
labelText: 'Select Course',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: 'flutter',
child: Text('Flutter Development'),
),
DropdownMenuItem(
value: 'web',
child: Text('Web Development'),
),
DropdownMenuItem(
value: 'python',
child: Text('Python Development'),
),
],
onChanged: (value) {
selectedCourse = value;
},
validator: (value) {
if (value == null) {
return 'Please select a course';
}
return null;
},
)
Flutter's FormField system is also used by widgets such as DropdownButtonFormField. :contentReference[oaicite:11]{index=11}
28. Checkbox in a Structured Form
Checkboxes can be used to collect boolean choices such as accepting terms and conditions.
bool acceptedTerms = false;
CheckboxListTile(
value: acceptedTerms,
title: const Text(
'I agree to the Terms and Conditions',
),
onChanged: (value) {
setState(() {
acceptedTerms = value ?? false;
});
},
)
Validate Checkbox
if (!acceptedTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Please accept the Terms and Conditions',
),
),
);
return;
}
29. Radio Buttons in a Structured Form
String? gender;
Column(
children: [
RadioListTile(
title: const Text('Male'),
value: 'male',
groupValue: gender,
onChanged: (value) {
setState(() {
gender = value;
});
},
),
RadioListTile(
title: const Text('Female'),
value: 'female',
groupValue: gender,
onChanged: (value) {
setState(() {
gender = value;
});
},
),
],
)
30. Date Selection in a Form
Date fields are useful for date of birth, appointment dates, delivery dates, and similar information.
DateTime? selectedDate;
Future selectDate(BuildContext context) async {
final date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(1950),
lastDate: DateTime.now(),
);
if (date != null) {
setState(() {
selectedDate = date;
});
}
}
Open Date Picker
ElevatedButton(
onPressed: () {
selectDate(context);
},
child: const Text('Select Date'),
)
31. Password Visibility
A structured account form often includes password fields. Password visibility can be controlled using a boolean state variable.
bool passwordVisible = false;
TextFormField(
obscureText: !passwordVisible,
decoration: InputDecoration(
labelText: 'Password',
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
passwordVisible
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
),
)
32. Structuring Forms with Helper Widgets
Very large forms can become difficult to maintain if every field is written directly inside one large build() method. Reusable helper widgets can improve readability.
Reusable Input Widget
Widget buildInputField({
required String label,
required TextEditingController controller,
TextInputType? keyboardType,
}) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '$label is required';
}
return null;
},
);
}
Using the Helper
Column(
children: [
buildInputField(
label: 'Name',
controller: nameController,
),
const SizedBox(height: 16),
buildInputField(
label: 'Email',
controller: emailController,
keyboardType: TextInputType.emailAddress,
),
],
)
33. Separating Form Sections
For large applications, each logical section can be implemented as a separate widget.
Column(
children: [
PersonalInformationSection(),
const SizedBox(height: 24),
ContactInformationSection(),
const SizedBox(height: 24),
AccountInformationSection(),
const SizedBox(height: 24),
TermsSection(),
],
)
This approach makes large forms easier to read, test, reuse, and maintain.
34. Complete Structured Registration Form
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class RegistrationScreen extends StatefulWidget {
const RegistrationScreen({super.key});
@override
State createState() =>
_RegistrationScreenState();
}
class _RegistrationScreenState
extends State {
final formKey = GlobalKey();
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final passwordController = TextEditingController();
final confirmPasswordController = TextEditingController();
String? selectedCourse;
bool acceptedTerms = false;
@override
void dispose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
passwordController.dispose();
confirmPasswordController.dispose();
super.dispose();
}
void submitForm() {
if (!formKey.currentState!.validate()) {
return;
}
if (!acceptedTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Please accept the Terms and Conditions',
),
),
);
return;
}
final name = nameController.text.trim();
final email = emailController.text.trim();
final phone = phoneController.text.trim();
print('Name: $name');
print('Email: $email');
print('Phone: $phone');
print('Course: $selectedCourse');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Registration successful'),
),
);
}
void resetForm() {
formKey.currentState!.reset();
nameController.clear();
emailController.clear();
phoneController.clear();
passwordController.clear();
confirmPasswordController.clear();
setState(() {
selectedCourse = null;
acceptedTerms = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Student Registration'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Form(
key: formKey,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Personal Information',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: nameController,
textCapitalization:
TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null ||
value.trim().isEmpty) {
return 'Enter your full name';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: emailController,
keyboardType:
TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null ||
value.trim().isEmpty) {
return 'Enter your email';
}
if (!value.contains('@')) {
return 'Enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null ||
value.trim().isEmpty) {
return 'Enter your phone number';
}
if (value.trim().length != 10) {
return 'Enter a 10-digit phone number';
}
return null;
},
),
const SizedBox(height: 30),
const Text(
'Course Information',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
DropdownButtonFormField(
decoration: const InputDecoration(
labelText: 'Select Course',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: 'flutter',
child: Text('Flutter Development'),
),
DropdownMenuItem(
value: 'web',
child: Text('Web Development'),
),
DropdownMenuItem(
value: 'python',
child: Text('Python Development'),
),
],
onChanged: (value) {
setState(() {
selectedCourse = value;
});
},
validator: (value) {
if (value == null) {
return 'Please select a course';
}
return null;
},
),
const SizedBox(height: 30),
const Text(
'Account Information',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null ||
value.isEmpty) {
return 'Enter a password';
}
if (value.length < 8) {
return 'Password must contain at least 8 characters';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: confirmPasswordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Confirm Password',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null ||
value.isEmpty) {
return 'Confirm your password';
}
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
},
),
const SizedBox(height: 20),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: acceptedTerms,
title: const Text(
'I agree to the Terms and Conditions',
),
onChanged: (value) {
setState(() {
acceptedTerms = value ?? false;
});
},
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: submitForm,
child: const Text('Register'),
),
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: resetForm,
child: const Text('Reset'),
),
),
],
),
),
),
),
);
}
}
35. Making Forms Scrollable
Large forms may contain more fields than can fit on the screen. Wrapping the form inside SingleChildScrollView allows users to scroll through all fields.
SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Form(
key: formKey,
child: Column(
children: [
TextFormField(),
TextFormField(),
TextFormField(),
TextFormField(),
TextFormField(),
],
),
),
)
This is particularly useful on mobile devices when the on-screen keyboard reduces the available height.
36. Responsive Structured Forms
A form should be comfortable to use on mobile, tablet, and desktop screens. On larger screens, the form can be given a maximum width.
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 600,
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Form(
key: formKey,
child: Column(
children: [
TextFormField(),
const SizedBox(height: 16),
TextFormField(),
],
),
),
),
),
)
37. Two-Column Form Layout
On larger screens, related fields can be displayed side by side.
Row(
children: [
Expanded(
child: TextFormField(
decoration: const InputDecoration(
labelText: 'First Name',
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
decoration: const InputDecoration(
labelText: 'Last Name',
),
),
),
],
)
For small screens, the fields can be stacked vertically instead.
38. Form Data Model
For larger applications, form data can be represented using a Dart model class instead of passing many individual variables around.
class Student {
final String name;
final String email;
final String phone;
final String course;
Student({
required this.name,
required this.email,
required this.phone,
required this.course,
});
}
Creating the Model from Form Data
final student = Student(
name: nameController.text.trim(),
email: emailController.text.trim(),
phone: phoneController.text.trim(),
course: selectedCourse!,
);
39. Submitting Structured Form Data
After validation, the collected data can be passed to a service, repository, API client, or database layer.
void submitForm() {
if (!formKey.currentState!.validate()) {
return;
}
final student = Student(
name: nameController.text.trim(),
email: emailController.text.trim(),
phone: phoneController.text.trim(),
course: selectedCourse!,
);
saveStudent(student);
}
void saveStudent(Student student) {
print(student.name);
print(student.email);
print(student.phone);
print(student.course);
}
40. Loading State During Submission
If submitting the form requires an asynchronous API call, use a loading state to prevent repeated submissions.
bool isLoading = false;
Future submitForm() async {
if (!formKey.currentState!.validate()) {
return;
}
setState(() {
isLoading = true;
});
try {
await Future.delayed(
const Duration(seconds: 2),
);
print('Data submitted successfully');
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
Submit Button
ElevatedButton(
onPressed: isLoading ? null : submitForm,
child: isLoading
? const CircularProgressIndicator()
: const Text('Submit'),
)
41. Structured Form Submission Flow
User enters information
↓
Input fields collect values
↓
Form validation starts
↓
Are all fields valid?
↙ ↘
No Yes
↓ ↓
Show errors Collect data
↓
Create model
↓
API request
↓
Success / Failure
↓
Show result
42. Handling Server-Side Errors
Client-side validation is useful for user experience, but applications should also handle errors returned by the backend.
Future submitForm() async {
if (!formKey.currentState!.validate()) {
return;
}
try {
await saveDataToServer();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data saved successfully'),
),
);
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Unable to save data'),
),
);
}
}
Future saveDataToServer() async {
await Future.delayed(
const Duration(seconds: 1),
);
}
43. Common Mistakes While Creating Structured Forms
Mistake 1: Recreating GlobalKey in build()
@override
Widget build(BuildContext context) {
final formKey = GlobalKey();
return Form(
key: formKey,
child: Container(),
);
}
For a stateful form, keep the key as a state field instead.
Mistake 2: Forgetting Validation
Do not submit important data without checking whether required fields and formats are valid.
Mistake 3: Forgetting to Dispose Controllers
@override
void dispose() {
nameController.dispose();
emailController.dispose();
super.dispose();
}
Mistake 4: Creating a Very Long Unstructured Form
Divide large forms into meaningful sections so users can understand what information is required.
Mistake 5: No Loading State
When an API request is running, protect the form from accidental repeated submissions.
Mistake 6: Poor Error Messages
Validation messages should clearly tell the user what needs to be corrected.
44. Best Practices for Structured Forms
- Use a
Form to group related form fields.
- Use
GlobalKey when direct form-state access is needed.
- Use
TextFormField for validated text input.
- Keep form sections logically organized.
- Use clear labels and hints.
- Use appropriate keyboard types.
- Validate required fields.
- Use input formatters for input restrictions.
- Use
TextEditingController when direct programmatic control is required.
- Dispose controllers and focus nodes owned by stateful widgets.
- Use scrolling for long forms.
- Use responsive layouts for different screen sizes.
- Disable repeated submission while an asynchronous request is running.
- Do not expose passwords or sensitive data through debug logs in production.
- Perform appropriate server-side validation as well.
45. Interview Questions
Q1. What is a Form in Flutter?
A Form is a widget used to group and manage multiple form fields.
Q2. What is a structured form?
A structured form is a logically organized collection of related input fields, validation rules, actions, and data-processing logic.
Q3. What is GlobalKey?
It is a key commonly used to access the current state of a Form and perform operations such as validation, saving, and resetting.
Q4. What is TextFormField?
TextFormField is a form-aware text input widget that integrates a text field with Flutter's FormField system.
Q5. How do you validate a form?
if (formKey.currentState!.validate()) {
// Form is valid.
}
Q6. What does validator return?
A validator returns an error message string when the field is invalid and null when the field is valid.
Q7. How do you save a form?
formKey.currentState!.save();
Q8. How do you reset a form?
formKey.currentState!.reset();
Q9. Why use TextEditingController?
It provides direct access to the current text and allows the application to read, change, or clear field values programmatically.
Q10. Why is focus management important?
It creates a smoother input flow by allowing users to move between fields efficiently, especially on mobile keyboards.
46. Practical Exercise
Create a complete Student Registration Application with a structured form containing:
- First Name
- Last Name
- Email
- Phone Number
- Date of Birth
- Gender
- Course
- Address
- Password
- Confirm Password
- Terms and Conditions
- Register Button
- Reset Button
Requirements
- Use
Form.
- Use
GlobalKey.
- Use
TextFormField for text input.
- Validate all required fields.
- Validate email format.
- Validate phone number.
- Validate password length.
- Confirm that both passwords match.
- Use a dropdown for course selection.
- Use a date picker for date of birth.
- Use radio buttons for gender.
- Use a checkbox for terms and conditions.
- Use
TextEditingController where programmatic access is required.
- Use
FocusNode for keyboard navigation.
- Make the form scrollable.
- Add a loading state during submission.
- Display a success message after successful submission.
- Provide a Reset button.
- Dispose controllers and focus nodes properly.
47. Quick Revision
| Requirement | Flutter Feature |
|---|
| Group fields | Form |
| Text input | TextFormField |
| Form field state | FormField |
| Access form state | GlobalKey |
| Validate form | formKey.currentState!.validate() |
| Save form | formKey.currentState!.save() |
| Reset form | formKey.currentState!.reset() |
| Read text | controller.text |
| Clear text | controller.clear() |
| Control focus | FocusNode |
| Restrict input | inputFormatters |
| Automatic validation | autovalidateMode |
| Field validation | validator |
| Save individual field | onSaved |
48. Key Takeaways
- Structured forms organize related user input into a single manageable unit.
- The
Form widget acts as a container for multiple form fields.
TextFormField integrates text input with the form-field system.
GlobalKey provides convenient access to form state.
validate() checks all validators in the form.
save() invokes the fields' onSaved callbacks.
reset() resets form fields and validation state.
TextEditingController provides programmatic control over text fields.
FocusNode helps create smooth keyboard navigation.
- Large forms should be divided into logical sections.
- Long forms should generally support scrolling.
- Responsive layouts should adapt to different screen sizes.
- Validation should happen before data is processed or submitted.
- Controllers and focus nodes should be disposed when they are no longer needed.
49. Official Flutter Resources
50. JustAcademy Flutter Training Resources
Learn more about Flutter development through the following resources: